/*
 * VHMethod.java
 * Created on 26 mai 2009
 *
 * VirtualHockey Internet Platform
 * http://code.google.com/p/virtualhockey/
 *
 * Copyright (c) 2005-2009, Jens and Jan Janke (VirtualHockey Project)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/.
 */

package com.hockeo.vhweb.server.convert;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.hockeo.vhbase.data.dao.IGEntity;
import com.hockeo.vhweb.client.utils.VHJsData;
import com.hockeo.vhweb.client.utils.VHServerException;

/**
 * Utility method that wraps around a {@link Method} object and that contains additional
 * information that is required by the {@link VHDaoConverter}.
 *
 * @version $Id$
 * @author jjanke
 */
final class VHMethod
{
  /**
   * Array containing all supported type converters.
   */
  private static final VHTypeConverter<?, ?>[] d_aTypeConverters   = { new VHBigDecimalConverter(),
      new VHCalendarConverter(), new VHEnumConverter()            };

  static final String                          SUFFIX_TRANSLATED   = "_translated";
  static final String                          SUFFIX_ENUM_ORDINAL = "_order";

  private static Log                           d_logger            = LogFactory.getLog( VHMethod.class );
  private final Method                         d_method;
  private final String                         d_strName;
  private VHTypeConverter<?, ?>                d_typeConverter     = null;
  private boolean                              d_fRequiresLocale   = false;

  VHMethod( Method method )
  {
    assert method.getName().startsWith( "get" ) || method.getName().startsWith( "is" ) : "Not a 'get' or 'is' method.";
    assert method.getName().length() > 3 : "Method name not longer than 3 characters.";
    assert method.getParameterTypes().length == 0
        || ( method.getParameterTypes().length == 1 && method.getParameterTypes()[0].equals( Locale.class ) ) : "Method must have no argument or a Locale.";

    d_method = method;

    // Locale parameter required?
    //
    if ( d_method.getParameterTypes().length == 1 )
      d_fRequiresLocale = true;

    // take the method name without the "get" and lower case of the first character
    //
    int iFirstChar = 0;
    if ( d_method.getName().startsWith( "get" ) )
      iFirstChar = 3;
    else if ( d_method.getName().startsWith( "is" ) )
      iFirstChar = 2;
    else
      throw new IllegalStateException( "The method name does not start with 'get' or 'is': " + d_method.getName() );

    d_strName = d_method.getName().substring( iFirstChar, iFirstChar + 1 ).toLowerCase()
        + d_method.getName().substring( iFirstChar + 1 ) + ( d_fRequiresLocale ? SUFFIX_TRANSLATED : "" );

    // Do we need a special type converter?
    //
    Class<?> clsReturn = d_method.getReturnType();
    for ( VHTypeConverter<?, ?> tc : d_aTypeConverters )
    {
      if ( tc.getSourceType().isAssignableFrom( clsReturn ) )
      {
        d_typeConverter = tc;
        break;
      }
    }
  }

  /**
   * Returns the "real" {@link Method} object ready for invocation.
   */
  final Method getMethod()
  {
    return d_method;
  }

  /**
   * Returns the name of the method without the 'get' prefix. If the method takes a Locale
   * as parameter, the text {@value #SUFFIX_TRANSLATED} is appended.
   */
  final String getName()
  {
    return d_strName;
  }

  /**
   * Invokes this method on the given object.
   *
   * @param obj the object on which to invoke this method
   * @param dest the destination object to which to add the method's return value
   * @param locale the locale to be used (if required)
   * @throws VHServerException if the invocation fails
   */
  Serializable invoke( Object obj, VHJsData dest, Locale locale ) throws VHServerException
  {
    Serializable val;

    try
    {
      Object[] aobjArgs = ( d_fRequiresLocale ? new Object[] { locale } : new Object[0] );

      val = (Serializable) d_method.invoke( obj, aobjArgs );
    }
    catch ( InvocationTargetException ex )
    {
      d_logger.fatal( "Method " + d_method.getName() + "' on '" + obj.getClass().getName() + "' threw an exception.",
          ex.getCause() );

      throw new VHServerException( "Invoking getter method '" + d_method.getName() + "' on '"
          + obj.getClass().getName() + "' threw an exception.", ex.getCause() );
    }
    catch ( Exception ex )
    {
      throw new VHServerException( "Failed invoking method '" + d_method.getName() + "' on '"
          + obj.getClass().getName() + "'.", ex );
    }

    if ( val != null )
    {
      // For enumerations, we also add the ordinal value
      //
      if ( Enum.class.isAssignableFrom( d_method.getReturnType() ) )
        dest.set( getName() + SUFFIX_ENUM_ORDINAL, ( (Enum<?>) val ).ordinal() );

      // Do we have to do some type conversions
      //
      if ( d_typeConverter != null )
        val = (Serializable) d_typeConverter.castAndConvert( val );
    }

    return val;
  }

  /**
   * <p>
   * Determines whether the given method is supported for invocation. It must be a method
   * whose name is prefixed by 'get' and that has a supported return type (see list
   * below).
   * </p>
   *
   * <ul>
   * <li>Primitive types (byte, char, short, int, long, float, double, boolean) and their
   * corresponding wrapper classes.</li>
   * <li>{@link String}</li>
   * <li>{@link Date}</li>
   * <li>{@link Calendar}</li>
   * <li>{@link BigDecimal}</li>
   * <li>{@link Enum}</li>
   * </ul>
   *
   * @param setExpandedDaos set of DAOs that are allowed as return types because they are
   *          directly expanded by the converter
   * @param m the method to be checked
   * @return <code>true</code> if the method is supported, otherwise <code>false</code>
   */
  @SuppressWarnings( "unchecked" )
  static boolean isSupported( Set<Class<? extends IGEntity>> setExpandedDaos, Method m )
  {
    if ( !m.getName().startsWith( "get" ) && !m.getName().startsWith( "is" ) )
      return false;

    Class<?> clsReturn = m.getReturnType();
    int nNumParams = m.getParameterTypes().length;

    if ( nNumParams == 0 )
    {
      if ( clsReturn.isPrimitive() )
        return true;
      else if ( String.class.isAssignableFrom( clsReturn ) )
        return true;
      else if ( Enum.class.isAssignableFrom( clsReturn ) )
        return true;
      else if ( Date.class.isAssignableFrom( clsReturn ) )
        return true;
      else if ( Calendar.class.isAssignableFrom( clsReturn ) )
        return true;
      else if ( BigDecimal.class.isAssignableFrom( clsReturn ) )
        return true;

      // test expandable DAOs (at first, is the class directly contained, then by checking
      // assignability to find potential super classes)
      //
      if ( setExpandedDaos.contains( clsReturn ) )
        return true;

      for ( Class<? extends IGEntity> cls : setExpandedDaos )
        if ( cls.isAssignableFrom( clsReturn ) )
        {
          // If the class is a subclass of one of the classes in the set, we just add it
          // (for performance reasons - to avoid further isAssignableFrom lookups)
          //
          setExpandedDaos.add( (Class<? extends IGEntity>) clsReturn );

          return true;
        }

      return false;
    }
    else if ( nNumParams == 1 )
    {
      if ( m.getParameterTypes()[0].equals( Locale.class ) && String.class.isAssignableFrom( clsReturn ) )
        return true;

      return false;
    }

    return false;
  }
}
